# -*- coding: utf-8 -*-

'''Objects representing PDF stroke and filling extracted from Path.
表示从Path提取的PDF描边和填充的对象。

* Stroke: consider only the horizontal or vertical path segments
* Stroke: 只考虑水平或垂直的路径段
* Fill  : bbox of closed path filling area
* Fill  : 闭合路径填充区域的边界框

Hyperlink in ``PyMuPDF`` is represented as uri and its rectangular area (hot-area), while the
applied text isn't extracted explicitly. To reuse the process that identifying applied text of
text style shape (e.g. underline and highlight), hyperlink is also abstracted to be a ``Shape``.
``PyMuPDF``中的超链接表示为uri及其矩形区域（热区），而应用的文本不会被明确提取。
为了重用识别文本样式形状（如下划线和高亮）的应用文本的过程，超链接也被抽象为``Shape``。

.. note::
    The evident difference of hyperlink shape to text style shape is: the ``type`` of hyperlink 
    shape is determined in advance, while text style shape needs to be identified by the position 
    to associated text blocks.
    超链接形状与文本样式形状的明显区别是：超链接形状的``type``是预先确定的，
    而文本样式形状需要通过与相关文本块的位置关系来识别。

Above all, the semantic meaning of shape instance may be:
总之，形状实例的语义含义可能是：

* strike through line of text
* 文本的删除线
* under line of text
* 文本的下划线
* highlight area of text
* 文本的高亮区域
* table border
* 表格边框
* cell shading
* 单元格底纹
* hyperlink
* 超链接

Data structure::
数据结构::

    {
        'type': int,
        'bbox': (x0, y0, x1, y1),
        'color': srgb_value,

        # for Stroke
        # 对于描边
        'start': (x0, y0),
        'end': (x1, y1),
        'width': float,

        # for Hyperlink
        # 对于超链接
        'uri': str
    }

.. note::
    These coordinates are relative to real page CS since they're extracted from ``page.get_drawings()``,
    which is based on real page CS. So, needn't to multiply Element.ROTATION_MATRIX when initializing
    from source dict.
    这些坐标是相对于真实页面坐标系的，因为它们是从基于真实页面坐标系的``page.get_drawings()``中提取的。
    因此，从源字典初始化时不需要乘以Element.ROTATION_MATRIX。
'''

import fitz
from ..common.Element import Element
from ..common.share import RectType
from ..common import constants


class Shape(Element):
    ''' Shape object.'''
    ''' 形状对象。'''
    def __init__(self, raw:dict=None):        
        raw = raw or {}
        self.color = raw.get('color', 0)
        
        # NOTE: coordinates are based on real page CS already
        # 注意：坐标已基于真实页面坐标系
        super().update_bbox(raw.get('bbox', (0,)*4))
        self._parent = None

        # shape semantic type
        # It's able to set shape type in ``RectType``, but a shape might belong to multi-types before
        # it's finalized. So, set int type here.
        # 形状语义类型
        # 可以在``RectType``中设置形状类型，但在最终确定之前，一个形状可能属于多种类型。
        # 因此，在这里设置整数类型。
        self._type = raw.get('type', -1)            # final type
                                                    # 最终类型
        self._potential_type = self.default_type    # potential types, a combination of raw RectType-s
                                                    # 潜在类型，原始RectType的组合


    @property
    def type(self): return self._type

    @type.setter
    def type(self, rect_type:RectType): self._type = rect_type.value

    @property
    def is_determined(self): 
        '''If the shape type is determined to a basic item of RectType.'''
        '''如果形状类型已确定为RectType的基本项。'''
        return self._type != -1

    def equal_to_type(self, rect_type:RectType): 
        '''If shape type is equal to the specified one or not.'''
        '''形状类型是否等于指定类型。'''
        return self._type==rect_type.value

    def has_potential_type(self, rect_type:RectType):
        '''If shape type has a chance to be the specified one or not.'''
        '''形状类型是否有可能是指定类型。'''
        return self._potential_type & rect_type.value

    @property
    def default_type(self): 
        '''Default semantic type for a shape.'''
        '''形状的默认语义类型。'''
        return sum(t.value for t in RectType)


    def store(self):
        res = super().store()
        res.update({
            'type': self._type,
            'color': self.color
        })
        return res


    def parse_semantic_type(self, blocks:list):
        '''Determine semantic type based on the position to text blocks. Note the results might be 
        a combination of raw types, e.g. the semantic type of a stroke can be either text strike,
        underline or table border.
        根据与文本块的位置关系确定语义类型。注意结果可能是原始类型的组合，
        例如，描边的语义类型可以是文本删除线、下划线或表格边框。

        Args:
            blocks (list): A list of ``Line`` instance, sorted in reading order in advance.
            blocks (list): 按阅读顺序预先排序的``Line``实例列表。
        '''
        for line in blocks:
            # not intersect yet
            # 尚未相交
            if line.bbox.y1 < self.bbox.y0: continue
            
            # no intersection any more
            # 不再有交集
            if line.bbox.y0 > self.bbox.y1: break

            # check it when intersected
            # 相交时检查
            rect_type = self._semantic_type(line)
            self._potential_type = rect_type

            if rect_type!=self.default_type: break


    def _semantic_type(self, line):
        ''' Check semantic type based on the position to a text line.
            Return all possibilities if can't be determined with this text line.
            Prerequisite: intersection exists between this shape and line.
            根据与文本行的位置关系检查语义类型。
            如果无法通过此文本行确定，则返回所有可能性。
            前提条件：此形状与行之间存在交集。
        '''
        return self.default_type


    def plot(self, page, color):
        '''Plot rectangle shapes with ``PyMuPDF``.'''
        '''使用``PyMuPDF``绘制矩形形状。'''
        page.draw_rect(self.bbox, color=color, fill=color, width=0, overlay=True)


class Stroke(Shape):
    ''' Horizontal or vertical stroke of a path. 
        The semantic meaning may be table border, or text style line like underline and strike-through.
        路径的水平或垂直描边。
        语义含义可能是表格边框，或者像下划线和删除线这样的文本样式线条。
    '''
    def __init__(self, raw:dict=None):
        raw = raw or {}
        # NOTE: real page CS
        # 注意：真实页面坐标系
        self._start = fitz.Point(raw.get('start', (0.0, 0.0)))
        self._end = fitz.Point(raw.get('end', (0.0, 0.0)))

        if self._start.x > self._end.x or self._start.y > self._end.y:
            self._start, self._end = self._end, self._start

        # width, color
        # 宽度，颜色
        super().__init__(raw) # type, color
        self.width = raw.get('width', 0.0) # Note this "width" is actually the height of stroke
                                          # 注意这个"width"实际上是描边的高度

        # update bbox
        # 更新边界框
        super().update_bbox(self._to_rect())


    @property
    def horizontal(self): return abs(self._start[1]-self._end[1])<1e-3

    @property
    def vertical(self): return abs(self._start[0]-self._end[0])<1e-3

    @property
    def x0(self): return self._start.x

    @property
    def x1(self): return self._end.x

    @property
    def y0(self): return self._start.y

    @property
    def y1(self): return self._end.y


    def update_bbox(self, rect):
        '''Update stroke bbox (related to real page CS).
        更新描边边界框（与真实页面坐标系相关）。

        * Update start/end points if ``rect.area==0``.
        * 如果``rect.area==0``，则更新起点/终点。
        * Ppdate bbox directly if ``rect.area!=0``.
        * 如果``rect.area!=0``，则直接更新边界框。

        Args:
            rect (fitz.Rect, tuple): ``(x0, y0, x1, y1)`` like data.
            rect (fitz.Rect, tuple): 类似``(x0, y0, x1, y1)``的数据。

        Returns:
            Stroke: self
        '''
        rect = fitz.Rect(rect)

        # an empty area line
        # 一条无面积的线
        if rect.get_area()==0.0:
            self._start = fitz.Point(rect[0:2])
            self._end = fitz.Point(rect[2:])
            super().update_bbox(self._to_rect())

        # a rect 
        # 一个矩形
        else:
            super().update_bbox(rect)

            # horizontal stroke
            # 水平描边
            if rect.width >= rect.height:
                y = (rect.y0+rect.y1)/2.0
                self._start = fitz.Point(rect.x0, y)
                self._end   = fitz.Point(rect.x1, y)

            # vertical stroke
            # 垂直描边
            else: 
                x = (rect.x0+rect.x1)/2.0
                self._start = fitz.Point(x, rect.y0)
                self._end   = fitz.Point(x, rect.y1)

        return self

    @property
    def default_type(self):
        '''Default semantic type for a Stroke shape: table border, underline or strike-through.'''
        '''Stroke形状的默认语义类型：表格边框、下划线或删除线。'''
        return RectType.BORDER.value | RectType.UNDERLINE.value | RectType.STRIKE.value

    def _semantic_type(self, line):
        '''Override. Check semantic type of a Stroke: table border v.s. text style line, e.g. underline 
        and strike-through. It's potentially a text style line when:
        重写。检查Stroke的语义类型：表格边框与文本样式线条（如下划线和删除线）。
        当满足以下条件时，它可能是文本样式线条：

        * the stroke and the text line has same orientation; and
        * 描边和文本行具有相同的方向；以及
        * the stroke never exceeds the text line along the main direction
        * 描边在主方向上永远不会超过文本行
        '''
        # check intersection
        # 检查交集
        expanded_shape = self.get_expand_bbox(2.0)
        if not line.bbox.intersects(expanded_shape): 
            return self.default_type        

        # check orientation
        # 检查方向
        h_shape = self.horizontal
        h_line = line.is_horizontal_text
        if h_shape != h_line: 
            return self.default_type

        # check main dimension
        # 检查主要维度
        line_x0, line_x1 = (line.bbox.x0, line.bbox.x1) if h_line else (line.bbox.y0, line.bbox.y1)
        shape_x0, shape_x1 = (self.bbox.x0, self.bbox.x1) if h_shape else (self.bbox.y0, self.bbox.y1)
        if shape_x0>=line_x0-1 and shape_x1<=line_x1+1: # 1 pt tolerance at both sides
                                                        # 两侧各有1点的容差
            return RectType.STRIKE.value | RectType.UNDERLINE.value
        else:
            return RectType.BORDER.value        


    def store(self):
        res = super().store()
        res.update({
            'start': tuple(self._start),
            'end': tuple(self._end),
            'width': self.width
        })
        return res


    def _to_rect(self):
        '''Convert centerline to rectangle shape.'''
        '''将中心线转换为矩形形状。'''
        h = self.width / 2.0
        x0, y0 = self._start
        x1, y1 = self._end        
        return (x0-h, y0-h, x1+h, y1+h)


class Fill(Shape):
    ''' Rectangular (bbox) filling area of a closed path. 
        The semantic meaning may be table shading, or text style like highlight.
        闭合路径的矩形（边界框）填充区域。
        语义含义可能是表格底纹，或者像高亮这样的文本样式。
    '''

    def to_stroke(self, max_border_width:float):
        '''Convert to Stroke instance based on width criterion.
        根据宽度标准转换为Stroke实例。

        Args:
            max_border_width (float): Stroke width must less than this value.
            max_border_width (float): 描边宽度必须小于此值。

        Returns:
            Stroke: Stroke instance.
            Stroke: Stroke实例。
        
        .. note::
            A Fill from shape point of view may be a Stroke from content point of view.
            The criterion here is whether the width is smaller than defined ``max_border_width``.
            从形状角度看是Fill，从内容角度看可能是Stroke。
            这里的标准是宽度是否小于定义的``max_border_width``。
        '''
        w = min(self.bbox.width, self.bbox.height)

        # not a stroke if exceed max border width
        # 如果超过最大边框宽度，则不是描边
        if w > max_border_width:
            return None
        else:
            return Stroke({'width': w, 'color': self.color}).update_bbox(self.bbox)
    

    @property
    def default_type(self):
        '''Default semantic type for a Fill shape: table shading or text highlight.'''
        '''Fill形状的默认语义类型：表格底纹或文本高亮。'''
        return RectType.SHADING.value | RectType.HIGHLIGHT.value
    
    def _semantic_type(self, line):
        '''Override. Check semantic type based on the position to a text line. Along the main dimension,
        text highlight never exceeds text line.
        重写。根据与文本行的位置关系检查语义类型。沿主要维度，文本高亮永远不会超过文本行。

        Args:
            line (Line): A text line.
            line (Line): 一个文本行。

        Returns:
            RectType: Semantic type of this shape.
            RectType: 此形状的语义类型。
        
        .. note::
            Generally, table shading always contains at least one line, while text highlight never
            contains any lines. But in real cases, with margin exists, table shading may not 100% 
            contain a line.
            通常，表格底纹总是至少包含一行，而文本高亮从不包含任何行。
            但在实际情况下，由于存在边距，表格底纹可能不会100%包含一行。
        '''
        # check main dimension
        # 检查主要维度
        h_shape = self.bbox.width>self.bbox.height
        w_shape = self.bbox.width if h_shape else self.bbox.height

        # check orientation
        # 检查方向
        h_line = line.is_horizontal_text
        if h_shape != h_line: 
            return self.default_type

        if not self.get_main_bbox(line, threshold=constants.FACTOR_MAJOR): 
            return self.default_type
        
        w_line = line.bbox.width if h_line else line.bbox.height            
        if w_shape <= w_line + 2*constants.MINOR_DIST: # 1 pt tolerance at both sides
                                                      # 两侧各有1点的容差
            return RectType.HIGHLIGHT.value
        else:
            return RectType.SHADING.value        


class Hyperlink(Shape):
    '''Rectangular area, i.e. ``hot area`` for a hyperlink. 
    矩形区域，即超链接的``热区``。
    
    Hyperlink in ``PyMuPDF`` is represented as uri and its hot area, while the applied text isn't extracted 
    explicitly. To reuse the process that identifying applied text of text style shape (e.g. underline and 
    highlight), hyperlink is also abstracted to be a ``Shape``.
    ``PyMuPDF``中的超链接表示为uri及其热区，而应用的文本不会被明确提取。
    为了重用识别文本样式形状（如下划线和高亮）的应用文本的过程，超链接也被抽象为``Shape``。
    '''

    def __init__(self, raw:dict=None):
        '''Initialize from raw dict. Note the type must be determined in advance.'''
        '''从原始字典初始化。注意类型必须预先确定。'''
        super().__init__(raw)

        # set uri
        # 设置uri
        self.uri = raw.get('uri', '')
        

    def store(self):
        res = super().store()
        res.update({
            'uri': self.uri
        })
        return res


    @property
    def default_type(self):
        '''Default semantic type for a Hyperlink: always hyperlink.'''
        '''Hyperlink的默认语义类型：始终是超链接。'''
        return RectType.HYPERLINK.value

    def parse_semantic_type(self, blocks:list=None):
        '''Semantic type of Hyperlink shape is determined, i.e. ``RectType.HYPERLINK``.'''
        '''Hyperlink形状的语义类型是确定的，即``RectType.HYPERLINK``。'''
        self._potential_type = self.default_type